package top.v5it.japi.plus.core.spring.web.interceptor;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import lombok.extern.slf4j.Slf4j;
import top.v5it.japi.plus.common.context.Authorized;
import top.v5it.japi.plus.common.context.AuthorizedContextHolder;
import top.v5it.japi.plus.common.util.AssertData;
import top.v5it.japi.plus.core.ConstantPool;
import top.v5it.japi.plus.core.cache.JapiCache;
import top.v5it.japi.plus.core.exception.JapiAuthorizedException;
import top.v5it.japi.plus.core.util.HttpServletUtil;
import top.v5it.japi.plus.log.OperLogService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static top.v5it.japi.plus.common.ConstantCommon.LOG_DIVIDING_LINE;

/**
 * 系统接入认证
 *
 * @author zhanpu
 * @date 21-1-22
 */
@Slf4j
public abstract class JapiAuthorizedInterceptor extends AbstractJapiInterceptor {

    /**
     * japiCache
     */
    protected final JapiCache japiCache;

    /**
     * nonce在允许范围内不可重复使用，单位秒
     */
    private final long timeout;

    /**
     * 客户端与服务端允许范围内的误差时间，单位秒
     */
    private final long diffTime;

    /**
     * 构造函数注入
     *
     * @param japiCache
     * @param operLogService 操作日志
     * @param timeout
     * @param diffTime
     */
    public JapiAuthorizedInterceptor(JapiCache japiCache, OperLogService operLogService, final long timeout, final long diffTime) {
        super(operLogService);
        this.japiCache = japiCache;
        this.timeout = timeout == 0L ? 30L : timeout;
        this.diffTime = diffTime == 0L ? 3000L : diffTime;
    }

    /**
     * 认证检验
     *
     * @param request   {@link HttpServletRequest}
     * @param response  {@link HttpServletResponse}
     * @param startTime 时间
     * @throws Exception 认证失败异常
     */
    @Override
    protected void preHandle(HttpServletRequest request, HttpServletResponse response, long startTime) throws Exception {

        // 将授权信息转换成map
        Map<String, String> authMap = HttpServletUtil.getAuthorization(request);

        if (log.isDebugEnabled()) {
            log.debug("{}请求头信息\n{}{}", LOG_DIVIDING_LINE, authMap, LOG_DIVIDING_LINE);
        }

        // 验证服务端证书序列号
        verifySerial(request);

        // 验证chnlid
        verifyChannel(authMap);

        // 验证timestamp和nonce_str
        verifyNonceStrAndTimestamp(authMap);

        // 验签
        verifySignature(request, authMap);

        // 后置处理
        prePostHandle(request, response, authMap, startTime);

        // 设置Authorized
        setAuthorized(request, authMap, startTime);
    }

    /**
     * 设置{@link Authorized}
     *
     * @param request   {@link HttpServletRequest}
     * @param authMap   {@link Map<String, String>}
     * @param startTime 开始时间
     */
    protected void setAuthorized(HttpServletRequest request, final Map<String, String> authMap, final long startTime) {
        // 设置Authorized，提供后续使用
        Authorized authorized = new Authorized()
                .setChannel(authMap.get(ConstantPool.CHANNEL_NAME))
                .setTimestamp(Long.parseLong(authMap.get(ConstantPool.TIMESTAMP_NAME)))
                .setRequestId(IdUtil.fastUUID())
                .setStartTime(startTime)
                .setClientSerialNo(authMap.get(ConstantPool.SERIAL_NAME))
                .setServerSerialNo(HttpServletUtil.getSerialNullable(request));

        AuthorizedContextHolder.setRequestAuthAttributes(authorized);
    }

    /**
     * 拼接待验签字符串
     *
     * <pre>
     *     ### 特殊情况，getQueryString 或者 getParameterMap 都能取到值
     *     GET http://localhost/v1/test-get/1?bb=yel
     *     Content-Type: application/x-www-form-urlencoded
     *
     *     name=jack&abc=masked
     *
     *     那么 url 为 /v1/test-get/1?bb=yel body 为 name=jack&abc=masked
     * </pre>
     *
     * @param request   {@link HttpServletRequest}
     * @param timestamp 时间戳，单位秒
     * @param nonceStr  随机串
     * @param body      请求体
     * @return 消息
     */
    protected String buildMessage(HttpServletRequest request, final String timestamp, final String nonceStr, final String body) {
        final String method = request.getMethod();
        final String queryStr = request.getQueryString();
        final Map<String, Object> parameterMap = HttpServletUtil.getParameterMap(request);
        String canonicalUrl = request.getRequestURI();
        if (StrUtil.isNotEmpty(queryStr)) canonicalUrl += ConstantPool.QUESTION + queryStr;
        final Map<String, Object> queryMap = HttpServletUtil.getQueryMap(request);
        queryMap.forEach((k, v) -> parameterMap.remove(k));
        final String paramsStr = HttpUtil.toParams(parameterMap);
        String newBody = body;
        newBody += URLUtil.decode(paramsStr);
        final String url = Method.GET.name().equals(method) ? URLUtil.decode(canonicalUrl) : canonicalUrl;
        return method + "\n"
                + url + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + newBody + "\n";
    }

    /**
     * 验证客户端请求上送的服务端证书序列号是否与服务端证书序列号一致
     *
     * @param request
     */
    protected void verifySerial(HttpServletRequest request) {

    }

    /**
     * 获取缓存接入渠道的key前缀
     *
     * @return 缓存key前缀
     */
    protected Object getCacheChannel(String channel) {
        return japiCache.getChannelInfo(ConstantPool.CHANNEL_NAME + ConstantPool.COLON + channel);
    }

    /**
     * 验证接入渠道
     *
     * @param authMap
     * @throws Exception
     */
    protected void verifyChannel(final Map<String, String> authMap) {
        final String channel =
                AssertData.notBlank(authMap.get(ConstantPool.CHANNEL_NAME)
                        , () -> new JapiAuthorizedException("请求头[{}]中[{}]不能为空", ConstantPool.AUTHORIZATION, ConstantPool.CHANNEL_NAME));
        log.debug("{}请求渠道号\n{}{}", LOG_DIVIDING_LINE, channel, LOG_DIVIDING_LINE);

        Object objV = getCacheChannel(channel);
        AssertData.notBlankIfStr(objV
                , () -> new JapiAuthorizedException("请求头[{}]中接入渠道编码[{}]无效", ConstantPool.AUTHORIZATION, ConstantPool.CHANNEL_NAME));
    }

    /**
     * 验证签名值
     *
     * @param request
     * @param authMap
     */
    protected abstract void verifySignature(HttpServletRequest request, final Map<String, String> authMap);

    /**
     * 验证随机数和时间
     *
     * @param authMap 授权信息
     */
    protected void verifyNonceStrAndTimestamp(final Map<String, String> authMap) {
        final String timestamp =
                AssertData.notBlank(authMap.get(ConstantPool.TIMESTAMP_NAME)
                        , () -> new JapiAuthorizedException("请求头[{}]中[{}]不能为空", ConstantPool.AUTHORIZATION, ConstantPool.TIMESTAMP_NAME));

        AssertData.isTrue(timestamp.length() == 10
                , () -> new JapiAuthorizedException("请求头[{}]中[{}]必须为10位数字的秒数", ConstantPool.AUTHORIZATION, ConstantPool.TIMESTAMP_NAME));

        final String nonceStr =
                AssertData.notBlank(authMap.get(ConstantPool.NONCE_NAME)
                        , () -> new JapiAuthorizedException("请求头[{}]中[{}]不能为空", ConstantPool.AUTHORIZATION, ConstantPool.NONCE_NAME));

        final long timestampLong = Long.parseLong(timestamp);
        final long now = Instant.now().getEpochSecond();
        final long diff = now - timestampLong;

        AssertData.isFalse(diff > diffTime
                , () -> new JapiAuthorizedException("请求头[{}]中[{}]无效，请求时间超过[{}]秒，请校对您的系统时间", ConstantPool.AUTHORIZATION, ConstantPool.TIMESTAMP_NAME, diffTime));

        // 将随机数据加入redis中
        final Boolean flag =
                japiCache.getRedisTemplate()
                        .opsForValue()
                        .setIfAbsent(ConstantPool.getCacheNoncePrefixKey() + nonceStr
                                , Boolean.TRUE.toString()
                                , timeout
                                , TimeUnit.SECONDS);

        AssertData.isTrue(Objects.nonNull(flag) && flag
                , () -> new JapiAuthorizedException("请求头[{}]中[{}]无效", ConstantPool.AUTHORIZATION, ConstantPool.NONCE_NAME));
    }
}
